package com.california.pay.service;

import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.TypeReference;
import com.california.pay.common.concurrent.LockTemplate;
import com.california.pay.common.utils.DateUtils;
import com.california.pay.common.utils.MD5Sign;
import com.california.pay.config.CommonErrors;
import com.california.pay.consts.NotifyStatus;
import com.california.pay.consts.OrderStatus;
import com.california.pay.exception.BusinessException;
import com.california.pay.facade.MchOrderNotifyFacade;
import com.california.pay.model.*;
import com.california.pay.persist.domain.MchPayQuota;
import com.california.pay.persist.domain.PayOrder;
import com.california.pay.persist.mapper.MchPayQuotaMapper;
import com.california.pay.persist.mapper.PayOrderMapper;
import com.california.pay.result.CommonResult;
import com.california.pay.socket.TransSocketMessage;
import com.google.common.collect.Maps;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.redisson.api.RMapCache;
import org.redisson.api.RedissonClient;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;

import java.time.LocalDateTime;
import java.util.Date;
import java.util.Map;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

@Slf4j
@Service
public class PayNotifyService extends BaseService{
    private final static String CHANNEL_MCH_FEE_LIMIT_KEY = "channel_mch_fee_limit:";
    @Autowired
    private PayOrderMapper payOrderMapper;
    @Autowired
    private MchPayQuotaMapper payQuotaMapper;
    @Autowired
    private MchOrderNotifyFacade notifyFacade;
    @Autowired
    private OrderPayService orderPayService;
    @Autowired
    private SessionService sessionService;
    @Autowired
    private TerminalLoginService loginService;
    @Autowired(required = false)
    private RedissonClient redissonClient;
    @Autowired(required = false)
    private LockTemplate lockTemplate;
    private ExecutorService notifyExecutorService = Executors.newCachedThreadPool();
    private ExecutorService notifyMchExecutorService = Executors.newCachedThreadPool();

    @Transactional(rollbackFor = {Exception.class})
    public Map<String, String> terminalPayNotify(TransSocketMessage socketMessage) {
        Date now = new Date();
        String message = socketMessage.getMessage();
        if (StringUtils.isBlank(message)) {
            throw new BusinessException(CommonErrors.INVALID_PARAM, "终端支付回调，message不为空");
        }

        Map<String, String> msgMap = JSON.parseObject(message, new TypeReference<Map<String, String>>() {
        });
        String payMemo = msgMap.get("payMemo");

        String channelOrderNo = msgMap.get("channelOrderNo");
        //校验sessionId有效性
        String clientId = msgMap.get("clientId");
        String sessionId = socketMessage.getSessionId();

        sessionService.validateSession(clientId, sessionId);
        validateReqSign(socketMessage, clientId);

        PayOrder payOrder = getPayOrder(payMemo);
        if (payOrder == null) {
            throw new BusinessException(CommonErrors.NOT_FUND_ORDER_NO, "未找到匹配订单号, innerPayNo=" + payMemo);
        }

        Map<String, String> rspMap = Maps.newHashMap();
        rspMap.put("acceptTime", DateUtils.yyyyMMddHHmmss(new Date()));
        rspMap.put("clientId", clientId);
        rspMap.put("channelOrderNo", channelOrderNo);

        if (!payOrder.getOrderStatus().equals(OrderStatus.ORDER_GEN)
                && !payOrder.getOrderStatus().equals(OrderStatus.ORDER_FAILED)) {
            return rspMap;
        }

        payOrder = updatePayOrderByTerminalNotify(payOrder.getOrderId(), msgMap, message);
        updateQuotaToDb(payOrder, now);
        notifyMch(payOrder);

        return rspMap;
    }

    public void notifyMch(PayOrder payOrder) {
        notifyExecutorService.submit(() -> {
            MchOrderPayNotifyReq notifyReq = getNotifyReq(payOrder, "终端支付结果反馈通知。");
            log.info("发送支付结果同步通知(return url)，notifyReq={}", JSON.toJSONString(notifyReq));
            try {
                CommonResult<MchOrderPayNotifyRsp> result = notifyFacade.payOrderNotify(notifyReq);
                log.info("发送支付结果同步通知(return url)，result={}", JSON.toJSONString(result));
                if (result.isSuccess()){
                    changeReturnStatusByNotify(payOrder, NotifyStatus.NOTIFY_SUCC);
                }else {
                    changeReturnStatusByNotify(payOrder, NotifyStatus.NOTIFY_FAIL);
                }

            } catch (Exception e) {
                log.error("支付return url 失败", e);
            }

        });

        notifyMchExecutorService.submit(()->{
            MchOrderPayNotifyMchReq notifyReq = getNotifyMchReq(payOrder, "终端支付结果反馈通知。");
            log.info("发送支付结果同步通知(notify url)，notifyMchReq={}", JSON.toJSONString(notifyReq));
            try {
                CommonResult<MchOrderPayNotifyMchRsp> result = notifyFacade.payOrderNotifyMch(notifyReq);
                log.info("发送支付结果同步通知(notify url)，result={}", JSON.toJSONString(result));
            } catch (Exception e) {
                log.error("支付notify url 失败", e);
            }

        });
    }

    public void notifyOrderToMch(PayOrder payOrder){
        notifyMchExecutorService.submit(()->{
            MchOrderPayNotifyMchReq notifyReq = getNotifyMchReq(payOrder, "终端支付结果反馈通知。");
            log.info("发送支付结果同步通知(notify url)，notifyMchReq={}", JSON.toJSONString(notifyReq));
            try {
                CommonResult<MchOrderPayNotifyMchRsp> result = notifyFacade.payOrderNotifyMch(notifyReq);
                log.info("发送支付结果同步通知(notify url)，result={}", JSON.toJSONString(result));
            } catch (Exception e) {
                log.error("支付notify url 失败", e);
            }

        });
    }

    @Transactional(rollbackFor = {Exception.class})
    public void changeReturnStatusByNotify(PayOrder payOrder, NotifyStatus notifyStatus){
        PayOrder updatePayOrder = new PayOrder();
        updatePayOrder.setOrderId(payOrder.getOrderId());
        updatePayOrder.setUpdateTime(new Date());
        updatePayOrder.setMchReturnStatus(notifyStatus);
        payOrderMapper.updateByPrimaryKeySelective(updatePayOrder);
    }


    private void validateReqSign(TransSocketMessage socketMessage, String clientId) {
        String appKey = loginService.getAppKey(clientId);

        String reqSocketBody = socketMessage.getSocketBody();
        String signIdx = "&signature=";
        if (StringUtils.contains(reqSocketBody, reqSocketBody)) {
            String rspsignBody = StringUtils.substring(reqSocketBody, 0, StringUtils.indexOf(reqSocketBody, signIdx));
            rspsignBody = rspsignBody + "appKey=" + appKey;
            String genMd5 = MD5Sign.md5(rspsignBody);
            if (!StringUtils.equals(genMd5, socketMessage.getSignature())) {
                log.error("请码校验错误(验签失败), clientId=" + clientId);
                throw new BusinessException(CommonErrors.SIGNATURE_VERIFY_FAIL, "请码校验错误(验签失败)");
            }
        } else {
            log.error("请码校验错误(未签名), clientId=" + clientId);
            throw new BusinessException(CommonErrors.SIGNATURE_VERIFY_FAIL, "请码校验错误(验签失败)");
        }

    }

    @Transactional(rollbackFor = {Exception.class})
    public OrderPayNotifyResultRsp mchNotifyAck(OrderPayNotifyResultReq notifyResultReq) {
        validate(notifyResultReq);
        PayOrder payOrder = orderPayService.getPayOrder(notifyResultReq.getInnerOrderNo());
        PayOrder updateOrder = new PayOrder();
        updateOrder.setOrderId(payOrder.getOrderId());
        updateOrder.setUpdateTime(new Date());
        updateOrder.setMchNotifyStatus(notifyResultReq.getNotifyStatus());
        payOrderMapper.updateByPrimaryKeySelective(updateOrder);

        OrderPayNotifyResultRsp rsp = new OrderPayNotifyResultRsp();
        rsp.setSystemTime(LocalDateTime.now());
        return rsp;
    }

    @Transactional(rollbackFor = {Exception.class})
    public void updateChannelMchQuota(PayOrder payOrder) {
        Date now = new Date();
        String channelMchId = payOrder.getChannelMchId();
        String mchDateKey = channelMchId + ":" + DateUtils.yyMMdd(now);

        lockTemplate.executeWithoutResult(mchDateKey, 3000, 30000, (lockFlag) -> {
            if (!lockFlag) {
                updateQuotaToDb(payOrder, now);
                return;
            }
            RMapCache<String, Long> feeMapCache = redissonClient.getMapCache(CHANNEL_MCH_FEE_LIMIT_KEY);
            Long fee = feeMapCache.get(mchDateKey);
            Long payFee = payOrder.getTxnAmount();

            if (fee == null || fee.equals(0L)) {
                fee = payFee;
            } else {
                fee += payFee;
            }

            feeMapCache.put(mchDateKey, fee);
        });

    }

    @Transactional(rollbackFor = {Exception.class}, propagation = Propagation.REQUIRES_NEW)
    public void updateQuotaToDb(PayOrder payOrder, Date now) {
        MchPayQuota query = new MchPayQuota();
        query.setChannelMchId(payOrder.getChannelMchId());
        query.setPayDate(payOrder.getPayDate());
        MchPayQuota record = payQuotaMapper.selectOne(query);
        String quotaId = record.getQuotaId();

        int count = payQuotaMapper.addPayAmountByPrimaryKey(payOrder.getTotalAmount(), quotaId);
/*        while (count == 0) {
            record = payQuotaMapper.selectOne(query);
            count = payQuotaMapper.addPayAmountByPrimaryKey(payOrder.getTotalAmount(), quotaId, record.getVersion());
        }*/
    }

    private MchOrderPayNotifyReq getNotifyReq(PayOrder payOrder, String message) {
        MchOrderPayNotifyReq req = new MchOrderPayNotifyReq();
        req.setOutOrderNo(payOrder.getMchOrderNo());
        req.setInnerOrderNo(payOrder.getInnerOrderNo());
        req.setTotalAmount(payOrder.getTotalAmount());
        req.setTxnAmount(payOrder.getTxnAmount());
        req.setChannelUserId(payOrder.getChannelUserId());
        req.setNotifyUrl(payOrder.getMchNotifyUrl());
        req.setReturnUrl(payOrder.getMchReturnUrl());
        req.setRemark(message);
        req.setSystemTime(LocalDateTime.now());
        return req;
    }

    private MchOrderPayNotifyMchReq getNotifyMchReq(PayOrder payOrder, String message) {
        MchOrderPayNotifyMchReq req = new MchOrderPayNotifyMchReq();
        req.setOutOrderNo(payOrder.getMchOrderNo());
        req.setInnerOrderNo(payOrder.getInnerOrderNo());
        req.setTotalAmount(payOrder.getTotalAmount());
        req.setTxnAmount(payOrder.getTxnAmount());
        req.setChannelUserId(payOrder.getChannelUserId());
        req.setNotifyUrl(payOrder.getMchNotifyUrl());
        req.setReturnUrl(payOrder.getMchReturnUrl());
        req.setRemark(message);
        req.setSystemTime(LocalDateTime.now());
        req.setInnerMchNo(payOrder.getInnerMchNo());
        return req;
    }

    private PayOrder getPayOrder(String innerOrderNo) {
        PayOrder query = new PayOrder();
        query.setInnerOrderNo(innerOrderNo);
        PayOrder payOrder = payOrderMapper.selectOne(query);
        return payOrder;
    }

    @Transactional(rollbackFor = {Exception.class}, propagation = Propagation.REQUIRES_NEW)
    public PayOrder updatePayOrderByTerminalNotify(String orderId, Map<String, String> msgMap, String message) {
        //String clientId = msgMap.get("clientId");
        Date now = new Date();
        String channelOrderNo = msgMap.get("channelOrderNo");
        String orderAmount = msgMap.get("orderAmount");
        //String payMemo = msgMap.get("payMemo");
        //String channelUserId = msgMap.get("channelUserId");
        String channelPayTime = msgMap.get("channelPayTime");
        Long payTime = Long.parseLong(channelPayTime);

        PayOrder payOrder = new PayOrder();
        payOrder.setOrderId(orderId);
        payOrder.setChannelOrderNo(channelOrderNo);
        payOrder.setOrderStatus(OrderStatus.PAY_SUCC);
        //payOrder.setChannelErrCode("");
        //payOrder.setChannelErrMsg("");
        payOrder.setChannelLastNotifyTime(payTime);
        payOrder.setTxnAmount(Long.parseLong(orderAmount));
        payOrder.setRemark(message);
        payOrder.setUpdateTime(now);
        payOrder.setPaySuccTime(payTime);

        Long lastDate = Long.parseLong(StringUtils.substring(channelPayTime, 0, 8));
        payOrder.setPayDate(lastDate);
        payOrder.setLastMchNotifyTime(Long.parseLong(DateUtils.yyyyMMddHHmmss(now)));

        payOrderMapper.updateByPrimaryKeySelective(payOrder);
        return payOrderMapper.selectByPrimaryKey(orderId);
    }


}
